﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using Jayden.Dll.Core.IO;
using System.IO;

namespace Jayden.Dll.BitTorrent
{
	public class BitTorrentPeer
	{
		public const string Protocol = "BitTorrent protocol";

		private bool m_AmChoking = true;
		private bool m_AmInterested = false;
		private bool m_Choking = true;
		private bool m_Interested = false;
		private byte[] m_Buffer;

		private TcpClient m_TcpClient;
		private IPAddress m_IpAddress;
		private short m_Port;
		private byte[] m_Id;
		private byte[] m_MyId;
		private BitTorrentFile m_File;
		private byte[] m_Extensions;
		private BitField m_BitField;

		public BitTorrentPeer(byte[] peerId, BitTorrentFile file, IPAddress ipAddress, short port)
		{
			m_File = file;
			m_MyId = peerId;
			m_IpAddress = ipAddress;
			m_Port = port;
		}

		public void Connect()
		{
			try
			{
				m_TcpClient = new TcpClient();
				m_TcpClient.BeginConnect(m_IpAddress.ToString(), (ushort)m_Port, OnBeginConnect, null);
			}
			catch (Exception e)
			{
				System.Console.WriteLine("Connect:" + e);
			}
		}

		private void OnBeginConnect(IAsyncResult result)
		{
			try
			{
				m_TcpClient.EndConnect(result);

				Handshake(new byte[8], m_File.InfoHash, m_MyId);
				//System.Console.WriteLine("\t   handshake sent");

				m_Buffer = new byte[49 + Protocol.Length];
				m_TcpClient.GetStream().BeginRead(m_Buffer, 0, m_Buffer.Length, OnReadHandshake, 0);
			}
			catch (Exception e)
			{
				if (m_TcpClient.Connected)
					System.Console.WriteLine("OnBeginConnect:" + e);
			}
		}

		private void OnReadHandshake(IAsyncResult result)
		{
			try
			{
				int read = m_TcpClient.GetStream().EndRead(result) + (int)result.AsyncState;
				if (read == m_Buffer.Length)
				{
					//System.Console.WriteLine("handshake received");
					// do something with handshake
					if(m_Buffer[0] != Protocol.Length)
					{
						System.Console.WriteLine("invalid : protocol lenght does not match ... closing connection.");
						m_TcpClient.Close();
						return;
					}
					for(int c=0;c<Protocol.Length;c++)
						if(m_Buffer[c + 1] != Protocol[c])
						{
							System.Console.WriteLine("invalid : protocol does not match ... closing connection.");
							m_TcpClient.Close();
							return;
						}
					m_Extensions = new byte[8];
					for (int c = 0; c < m_Extensions.Length; c++)
						m_Extensions[c] = m_Buffer[m_Buffer[0] + 1 + c];
					for (int c = 0; c < m_File.InfoHash.Length; c++)
						if (m_File.InfoHash[c] != m_Buffer[m_Buffer[0] + 9 + c])
						{
							System.Console.WriteLine("invalid : infohash does not match ... closing connection.");
							m_TcpClient.Close();
							return;
						}
					m_Id = new byte[20];
					for (int c = 0; c < m_Id.Length; c++)
						m_Id[c] = m_Buffer[m_Buffer[0] + 29 + c];
					// do something with handshake
					m_Buffer = new byte[4];
					m_TcpClient.GetStream().BeginRead(m_Buffer, 0, m_Buffer.Length, OnReadMessageSize, 0);
				}
				else
				{
					m_TcpClient.GetStream().BeginRead(m_Buffer, read, m_Buffer.Length - read, OnReadHandshake, read);
				}
			}
			catch (Exception e)
			{
				if(m_TcpClient.Connected)
					System.Console.WriteLine("OnReadHandshake:" + e);
			}
		}

		private void OnReadMessageSize(IAsyncResult result)
		{
			try
			{
				int read = m_TcpClient.GetStream().EndRead(result) + (int)result.AsyncState;
				if (read == m_Buffer.Length)
				{
					MemoryStream stream = new MemoryStream(m_Buffer);
					using (BigEndianReader reader = new BigEndianReader(stream))
					{
						int messageLength = reader.ReadInt32();
						int maxMessageLength = (m_AmInterested && !m_Choking) ? m_File.MaxMessageLength : m_BitField == null ? m_File.BitFieldMessageLength : 13;
						if (messageLength > maxMessageLength)
						{
							System.Console.WriteLine("supsicious : {0} bytes for message storage (max allowed is {1}) ... closing connection.", messageLength, maxMessageLength);
							m_TcpClient.Close();
							return;
						}
						if (messageLength > 0)
						{
							//System.Console.WriteLine("trying to allocate {0} bytes for message storage.", messageLength);
							m_Buffer = new byte[messageLength];
							m_TcpClient.GetStream().BeginRead(m_Buffer, 0, m_Buffer.Length, OnReadMessage, 0);
						}
						else
						{
							KeepAlive();
							// keep alive
							m_Buffer = new byte[4];
							m_TcpClient.GetStream().BeginRead(m_Buffer, 0, m_Buffer.Length, OnReadMessageSize, 0);
						}
					}
				}
				else
				{
					m_TcpClient.GetStream().BeginRead(m_Buffer, read, m_Buffer.Length - read, OnReadMessageSize, read);
				}
			}
			catch (Exception e)
			{
				if (m_TcpClient.Connected)
					System.Console.WriteLine("OnReadMessageSize:" + e);
			}
		}
		private void OnReadMessage(IAsyncResult result)
		{
			try
			{
				int read = m_TcpClient.GetStream().EndRead(result) + (int)result.AsyncState;
				if (read == m_Buffer.Length)
				{
					using (MemoryStream stream = new MemoryStream(m_Buffer))
					{
						using (BigEndianReader reader = new BigEndianReader(stream))
						{
							byte messageId = reader.InnerReader.ReadByte();
							switch (messageId)
							{
								case 0:
									System.Console.WriteLine("choked :s");
									m_Choking = true;
									break;
								case 1:
									System.Console.WriteLine("unchoked !!");
									m_Choking = false;
									break;
								case 2:
									m_Interested = true;
									break;
								case 3:
									m_Interested = false;
									break;
								case 4:
									OnReadHave(reader.ReadInt32());
									break;
								case 5:
									OnReadBitField();
									break;
								default:
									System.Console.WriteLine("unhadled message #{0} received", messageId);
									break;
							}
						}
					}
					m_Buffer = new byte[4];
					m_TcpClient.GetStream().BeginRead(m_Buffer, 0, m_Buffer.Length, OnReadMessageSize, 0);
				}
				else
				{
					m_TcpClient.GetStream().BeginRead(m_Buffer, read, m_Buffer.Length - read, OnReadMessage, read);
				}
			}
			catch (Exception e)
			{
				if (m_TcpClient.Connected)
					System.Console.WriteLine("OnReadMessage:" + e);
			}
		}

		private void OnReadHave(int pieceIndex)
		{
			if (m_BitField == null)
			{
				Interested();
				m_BitField = new BitField(m_File.PiecesCount);
			}
			m_BitField[pieceIndex] = true;
		}
		private void OnReadBitField()
		{
			if (m_Buffer.Length != m_File.BitFieldMessageLength)
			{
				System.Console.WriteLine("supsicious : {0} bytes for bitfield (expected is {1}) ... closing connection.", m_Buffer.Length, m_File.BitFieldMessageLength);
				m_TcpClient.Close();
				return;
			}
			m_BitField = new BitField(m_File.PiecesCount, m_Buffer, 1, m_Buffer.Length - 1);
			Interested();
		}

		public void Handshake(byte[] extensions, byte[] infoHash, byte[] peerId)
		{
			NetworkStream stream = m_TcpClient.GetStream();
			using (MemoryStream memoryStream = new MemoryStream())
			{
				using (BigEndianWriter writer = new BigEndianWriter(memoryStream))
				{
					writer.InnerWriter.Write((byte)Protocol.Length);
					for (int c = 0; c < Protocol.Length; c++)
						writer.InnerWriter.Write((byte)Protocol[c]);
					writer.InnerWriter.Write(extensions);
					writer.InnerWriter.Write(infoHash);
					writer.InnerWriter.Write(peerId);
					writer.InnerWriter.Flush();
					stream.Write(memoryStream.ToArray(), 0, (int)memoryStream.Position);
				}
			}
			stream.Flush();
		}

		public void SendMessage(byte[] message)
		{
			NetworkStream stream = m_TcpClient.GetStream();
			using (BigEndianWriter writer = new BigEndianWriter(stream))
			{
				writer.Write((int)message.Length);
				if (message.Length > 0)
					writer.InnerWriter.Write(message);
				writer.InnerWriter.Flush();
			}
			stream.Flush();
		}
		public void SendMessage(byte id) { SendMessage(new byte[] { 0 }); }

		public void KeepAlive() { SendMessage(new byte[0]); }
		public void Choke() { SendMessage((byte)0); }
		public void Unchoke() { SendMessage((byte)1); }
		public void Interested() { SendMessage((byte)2); }
		public void NotInterested() { SendMessage((byte)3); }
	}
}
